﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace Tierless.Framework.Client
{
	/// <summary> Queues work onto a single worker thread. </summary>
	public class WorkerQueue
	{
		private Queue<System.Action> _asyncQueue = new Queue<System.Action>();
		private Thread _asyncThread;

		/// <summary> Event which is executed (on the async thread) each time the async operation queue becomes empty. </summary>
		public event EventHandler AsyncOperationsComplete;

		/// <summary> Event which is executed (on the async thread) each time the async operation queue begins to be serviced. </summary>
		public event EventHandler AsyncOperationsStarted;

		public void Queue(System.Action action)
		{
			if (action != null)
			{
				var executeSync = false;

				lock (_asyncQueue)
				{
					if (_asyncThread == null)
					{
						_asyncThread = new Thread(new ThreadStart(AsyncQueueServiceThread));
						_asyncThread.Start();
					}
					else
						executeSync = (_asyncThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId);

					if (!executeSync)
						_asyncQueue.Enqueue(action);
				}

				if (executeSync)
				{
					action();
					DoAsyncOperationsComplete();
				}
			}
		}

		private void AsyncQueueServiceThread()
		{
			DoAsyncOperationsStarted();

			while (true)
			{
				System.Action nextAction;
				lock (_asyncQueue)
				{
					if (_asyncQueue.Count > 0)
						nextAction = _asyncQueue.Dequeue();
					else
					{
						_asyncThread = null;
						break;
					}
				}
				try
				{
					nextAction();
				}
				catch (Exception exception)
				{
					System.Diagnostics.Debug.WriteLine(exception.ToString());
					// Don't allow exceptions to leave this thread or the application will terminate
				}
			}

			DoAsyncOperationsComplete();
		}

		private void DoAsyncOperationsStarted()
		{
			try
			{
				if (AsyncOperationsStarted != null)
					AsyncOperationsStarted(this, EventArgs.Empty);
			}
			catch (Exception exception)
			{
				System.Diagnostics.Debug.WriteLine(exception.ToString());
				// Don't allow exceptions to leave this thread or the application will terminate
			}
		}

		/// <summary> Notifies that all async operations are complete. </summary>
		private void DoAsyncOperationsComplete()
		{
			try
			{
				if (AsyncOperationsComplete != null)
					AsyncOperationsComplete(this, EventArgs.Empty);
			}
			catch (Exception exception)
			{
				System.Diagnostics.Debug.WriteLine(exception.ToString());
				// Don't allow exceptions to leave this thread or the application will terminate
			}
		}

		/// <summary> Synchronously executes the given delegate on the main thread and returns the result. </summary>
		/// <remarks> This overload is to prevent the necessity to cast simple action lambdas to Action. </remarks>
		public object WaitOn(Action action)
		{
			return WaitOn((Delegate)action);
		}

		/// <summary> Synchronously executes the given delegate on the main thread and returns the result. </summary>
		/// <remarks> Safe to run on the main thread as well as the worker thread. </remarks>
		public object WaitOn(Delegate delegateValue, params object[] arguments)
		{
			object result = null;
			Exception exception = null;
			var eventValue = new ManualResetEvent(false);
			MainThread.Invoke
			(
				() =>
				{
					try
					{
						result = delegateValue.DynamicInvoke(arguments);
					}
					catch (Exception error)
					{
						exception = error;
					}
					finally
					{
						eventValue.Set();
					}
				}
			);
			eventValue.WaitOne();
			eventValue.Close();
			if (exception != null)
				throw exception;
			return result;
		}

		/// <summary> Take no further action after the current worker action completes. </summary>
		public void Clear()
		{
			lock (_asyncQueue)
			{
				_asyncQueue.Clear();
			}
		}

		/// <summary> Executes a set of actions, returning only when all actions have completed (or errored). </summary>
		/// <remarks> In the case of one or more exceptions being thrown by actions, the last exception
		/// throw will be re-thrown on the calling thread. </remarks>
		public void DoMany(Action[] actions)
		{
			// Create a set of wait handles
			var events = new ManualResetEvent[actions.Length];
			for (int i = 0; i < events.Length; i++)
				events[i] = new ManualResetEvent(false);
			
			Exception exception = null;
			for (int i = 0; i < actions.Length; i++)
			{
				ThreadPool.QueueUserWorkItem
				(
					(s) =>
					{
						try
						{
							if (actions[i] != null)
								actions[i]();
						}
						catch (Exception localException)
						{
							exception = localException;
						}
						events[i].Set();
					}
				);
			}

			WaitHandle.WaitAll(events);

			// Clean up all wait handles
			for (int i = 0; i < events.Length; i++)
				events[i].Close();

			// Re-throw last caught exception
			if (exception != null)
				throw exception;
		}
	}
}
